import {
  applicationToEnvVariable,
  detectApplicationType,
  ensureLoggableError,
  findConfigurationFile,
  findRuntimeConfigurationFile,
  getExecutableId,
  getRoot,
  loadConfigurationFile as loadRawConfigurationFile,
  logFatalError,
  parseArgs,
  saveConfigurationFile
} from '@platformatic/foundation'
import { loadConfiguration } from '@platformatic/runtime'
import { bold } from 'colorette'
import { execa } from 'execa'
import { existsSync } from 'node:fs'
import { readFile, writeFile } from 'node:fs/promises'
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
import { parseEnv } from 'node:util'
import { version } from '../version.js'
import { installDependencies } from './dependencies.js'

const originCandidates = ['origin', 'upstream']

function parseGitUrl (url) {
  const fragmentIndex = url.indexOf('#')

  if (fragmentIndex === -1) {
    return { url, branch: null }
  }

  const baseUrl = url.substring(0, fragmentIndex)
  const branch = url.substring(fragmentIndex + 1)

  return { url: baseUrl, branch }
}

async function parseLocalFolder (path) {
  // Read the package.json, if any
  const packageJsonPath = resolve(path, 'package.json')
  let packageJson
  try {
    packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'))
  } catch {
    packageJson = {}
  }

  let url

  // Detect if there is a git folder and eventually get the remote
  for (const candidate of originCandidates) {
    try {
      const result = await execa('git', ['remote', 'get-url', candidate], {
        cwd: path,
        env: { GIT_DIR: join(path, '.git') }
      })
      url = result.stdout.trim()
      break
    } catch (e) {
      // No-op
    }
  }

  // Check which capability we should use
  const appType = await detectApplicationType(path, packageJson)

  if (!appType) {
    return
  }

  const { name: capability, label } = appType

  return { id: packageJson.name ?? basename(path), url, packageJson, capability, label }
}

export async function appendEnvVariable (envFile, key, value) {
  let contents = ''

  if (existsSync(envFile)) {
    contents = await readFile(envFile, 'utf-8')

    if (contents.length && !contents.endsWith('\n')) {
      contents += '\n'
    }
  }

  contents += `${key}=${value}\n`

  return writeFile(envFile, contents, 'utf-8')
}

async function fixConfiguration (logger, root, configOption, skipDependencies, packageManager) {
  const configurationFile = await findRuntimeConfigurationFile(logger, root, configOption, true, true, false)

  /* c8 ignore next 3 - Hard to test */
  if (!configurationFile) {
    return
  }

  // Check if the configuration file was autogenerated
  const rawConfig = await loadRawConfigurationFile(configurationFile)
  let url

  try {
    url = new URL(rawConfig.$schema)
    /* c8 ignore next 3 - Hard to test */
  } catch (error) {
    // No-op
  }

  let applications

  /* c8 ignore next 4 - covered */
  if (url?.searchParams.get('autogenerated') === 'true') {
    const path = resolve(root)
    applications = [{ path, id: basename(path) }]
  } else {
    const config = await loadConfiguration(configurationFile)

    /* c8 ignore next 3 - Hard to test */
    if (!config) {
      return
    }

    applications = config.applications
  }

  // For each application, if there is no watt.json, create one and fix package dependencies
  for (const { path } of applications) {
    const wattConfiguration = await findConfigurationFile(path, 'application')

    const appType = await parseLocalFolder(resolve(root, path))

    /* c8 ignore next 3 - covered */
    if (!appType) {
      return logFatalError(logger, `The path ${bold(path)} does not contain a supported application.`)
    }

    const { id, packageJson, capability, label } = appType

    if (!wattConfiguration) {
      const wattJson = {
        $schema: `https://schemas.platformatic.dev/${capability}/${version}.json`
      }

      await saveConfigurationFile(resolve(path, 'watt.json'), wattJson)
    }

    packageJson.dependencies ??= {}

    if (!packageJson.dependencies[capability]) {
      packageJson.dependencies[capability] = `^${version}`
      packageJson.devDependencies ??= {}
      packageJson.devDependencies[capability] = undefined

      if (capability === '@platformatic/node') {
        logger.info(
          `Application ${bold(id)} is a ${bold('generic Node.js application')}. Adding ${bold(capability)} to its package.json dependencies.`
        )
      } else {
        logger.info(
          `Application ${bold(id)} is using ${bold(label)}. Adding ${bold(capability)} to its package.json dependencies.`
        )
      }

      await saveConfigurationFile(resolve(path, 'package.json'), packageJson)
    }
  }

  if (!skipDependencies) {
    return await installDependencies(logger, root, applications, false, packageManager)
  }
}

async function importApplication (logger, configurationFile, id, path, url, branch) {
  const config = await loadConfiguration(configurationFile)

  /* c8 ignore next 3 - Hard to test */
  if (!config) {
    return
  }

  const rawConfig = await loadRawConfigurationFile(configurationFile)
  const root = dirname(configurationFile)
  const envFile = resolve(root, '.env')
  const envSampleFile = resolve(root, '.env.sample')
  const envVariable = applicationToEnvVariable(id)

  let useEnv = true

  // If there is a locale path
  if (path) {
    let autoloadPath = config.autoload?.path

    // If we already autoload this path, there is nothing to do
    if (autoloadPath) {
      autoloadPath = resolve(root, autoloadPath)
      if (path.startsWith(autoloadPath)) {
        logger.warn('The path is already autoloaded as an application.')
        return
      }
    }

    // If the path is within the application repository
    if (path.startsWith(root)) {
      // If the path is already defined as an application, there is nothing to do
      if (config.applications.some(s => s.path === path)) {
        logger.warn('The path is already defined as an application.')
        return
      }

      // Do not use env variables
      useEnv = false
    }

    if (!url) {
      logger.warn(`The application ${bold(id)} does not define a Git repository.`)
    }
  }

  // Make sure the application is not already defined
  if (config.applications.find(app => app.id === id)) {
    return logFatalError(
      logger,
      `There is already an application ${bold(id)} defined, please choose a different application ID.`
    )
  }

  /* c8 ignore next */
  rawConfig.web ??= []

  if (useEnv) {
    const resolveConfig = { id, path: `{${envVariable}}`, url }

    if (resolveConfig.url && branch) {
      resolveConfig.gitBranch = branch
    }

    rawConfig.web.push(resolveConfig)

    // Make sure the environment variable is not already defined
    if (existsSync(envFile)) {
      const env = parseEnv(await readFile(envFile, 'utf-8'))

      if (env[envVariable]) {
        return logFatalError(
          logger,
          `There is already an environment variable ${bold(envVariable)} defined, please choose a different application ID.`
        )
      }
    }

    // Copy the .env file to .env.sample if it does not exist
    if (!existsSync(envSampleFile)) {
      await writeFile(envSampleFile, '', 'utf-8')
    }

    await appendEnvVariable(envFile, envVariable, path ?? '')
    await appendEnvVariable(envSampleFile, envVariable, '')
  } else {
    rawConfig.web.push({ id, path: relative(root, path) })
  }

  await saveConfigurationFile(configurationFile, rawConfig)
  return true
}

async function importURL (logger, _, configurationFile, rawUrl, id, http, branch) {
  let url = rawUrl
  if (rawUrl.match(/^[a-z0-9-]+\/[a-z0-9-]+$/i)) {
    url = http ? `https://github.com/${rawUrl}.git` : `git@github.com:${rawUrl}.git`
  }

  const parsed = parseGitUrl(url)
  const effectiveBranch = branch ?? parsed.branch

  await importApplication(
    logger,
    configurationFile,
    id ?? basename(parsed.url, '.git'),
    null,
    parsed.url,
    effectiveBranch
  )
}

async function importLocal (logger, root, configurationFile, path, overridenId) {
  const appType = await parseLocalFolder(resolve(root, path))

  if (!appType) {
    return logFatalError(logger, `The path ${bold(path)} does not contain a supported application.`)
  }

  const { id, url, packageJson, capability } = appType

  if (!(await importApplication(logger, configurationFile, overridenId ?? id, path, url))) {
    return
  }

  // Check if there is any configuration file we recognize. If so, don't do anything
  const wattConfiguration = await findConfigurationFile(path, 'application')

  if (wattConfiguration) {
    /* c8 ignore next */
    const displayPath = isAbsolute(path) ? path : relative(root, path)

    logger.info(
      `Path ${bold(resolve(displayPath, wattConfiguration))} already exists. Skipping configuration management ...`
    )
  } else {
    const wattJson = {
      $schema: `https://schemas.platformatic.dev/${capability}/${version}.json`
    }

    await saveConfigurationFile(resolve(path, 'watt.json'), wattJson)
  }

  packageJson.dependencies ??= {}
  packageJson.dependencies[capability] = `^${version}`

  logger.info(
    `Detected capability ${bold(capability)} for application ${bold(id)}, adding to the application dependencies.`
  )

  await saveConfigurationFile(resolve(path, 'package.json'), packageJson)
}

export async function resolveApplications (
  logger,
  root,
  configurationFile,
  username,
  password,
  skipDependencies,
  packageManager
) {
  const config = await loadConfiguration(configurationFile)

  /* c8 ignore next 3 - Hard to test */
  if (!config) {
    return
  }

  // The applications which might be to be resolved are the one that have a URL and either
  // no path defined (which means no environment variable set) or a non-existing path (which means not resolved yet)
  const resolvableApplications = config.applications.filter(application => {
    if (!application.url) {
      return false
    }

    if (application.path && existsSync(application.path)) {
      logger.warn(`Skipping application ${bold(application.id)} as the path already exists.`)
      return false
    }

    return true
  })

  // Iterate the applications a first time to verify the environment files configuration and which applications must be resolved
  const toResolve = []

  // Simply use application.path here
  for (const application of resolvableApplications) {
    if (!application.path) {
      application.path = resolve(root, `${config.resolvedApplicationsBasePath}/${application.id}`)
    }

    const directory = resolve(root, application.path)

    // If the directory already exists, it's either external or already resolved, nothing to do in both cases
    if (!existsSync(directory)) {
      if (!directory.startsWith(root)) {
        logger.warn(
          `Skipping application ${bold(application.id)} as the non existent directory ${bold(
            application.path
          )} is outside the project directory.`
        )
      } else {
        // This repository must be resolved
        toResolve.push(application)
      }
    } else {
      logger.warn(
        `Skipping application ${bold(application.id)} as the generated path ${bold(
          join(config.resolvedApplicationsBasePath, application.id)
        )} already exists.`
      )
    }
  }

  // Resolve the applications
  for (const application of toResolve) {
    const childLogger = logger.child({ name: application.id })

    try {
      const absolutePath = application.path
      const relativePath = relative(root, absolutePath)

      childLogger.info(`Resolving application ${bold(application.id)} ...`)

      const parsedGitUrl = parseGitUrl(application.url)
      let url = parsedGitUrl.url
      const effectiveBranch = application.gitBranch ?? parsedGitUrl.branch

      if (url.startsWith('http') && username && password) {
        const parsed = new URL(url)
        parsed.username ||= username
        parsed.password ||= password
        url = parsed.toString()
      }

      const cloneArgs = ['clone', url, absolutePath, '--single-branch', '--depth', 1]

      let branchLabel = ''
      if (effectiveBranch && effectiveBranch !== 'main') {
        cloneArgs.push('--branch', effectiveBranch)
        branchLabel = ` (branch ${bold(effectiveBranch)})`
      }

      if (username) {
        childLogger.info(
          `Cloning ${bold(application.url)}${branchLabel} as user ${bold(username)} into ${bold(relativePath)} ...`
        )
      } else {
        childLogger.info(`Cloning ${bold(application.url)}${branchLabel} into ${bold(relativePath)} ...`)
      }

      await execa('git', cloneArgs)
    } catch (error) {
      return logFatalError(
        childLogger,
        { error: ensureLoggableError(error) },
        `Unable to clone repository of the application ${bold(application.id)}`
      )
    }
  }

  // Install dependencies
  if (!skipDependencies) {
    return await installDependencies(logger, root, toResolve, false, packageManager)
  }
  /* c8 ignore next - Mistakenly reported as uncovered by C8 */
  return true
}

export async function importCommand (logger, args) {
  const {
    values: { config, id, http, branch, 'skip-dependencies': skipDependencies, 'package-manager': packageManager },
    positionals
  } = parseArgs(
    args,
    {
      config: {
        type: 'string',
        short: 'c'
      },
      id: {
        type: 'string',
        short: 'i'
      },
      http: {
        type: 'boolean',
        short: 'H'
      },
      branch: {
        type: 'string',
        short: 'b'
      },
      'skip-dependencies': {
        type: 'boolean',
        short: 's',
        default: false
      },
      'package-manager': {
        type: 'string',
        short: 'P'
      }
    },
    false
  )

  let root
  let rawUrl

  /*
    No arguments = Fix configuration for existing applications.
    One argument = URL
    Two arguments = root and URL
  */
  if (positionals.length === 0) {
    return fixConfiguration(logger, '', config, skipDependencies, packageManager)
  } else if (positionals.length === 1) {
    root = getRoot()
    rawUrl = positionals[0]
  } else {
    root = getRoot(positionals)
    rawUrl = positionals[1]
  }

  const configurationFile = await findRuntimeConfigurationFile(logger, root, config)

  /* c8 ignore next 3 - Hard to test */
  if (!configurationFile) {
    return
  }

  // If the rawUrl exists as local folder, import a local folder, otherwise go for Git.
  // Try a relative from the root folder or from process.cwd().
  const local = [resolve(root, rawUrl), resolve(process.cwd(), rawUrl)].find(c => {
    return existsSync(c)
  })

  if (local) {
    return importLocal(logger, root, configurationFile, local, id)
  }

  return importURL(logger, root, configurationFile, rawUrl, id, http, branch)
}

export async function resolveCommand (logger, args) {
  const {
    values: { config, username, password, 'skip-dependencies': skipDependencies, 'package-manager': packageManager },
    positionals
  } = parseArgs(
    args,
    {
      config: {
        type: 'string',
        short: 'c'
      },
      username: {
        type: 'string',
        short: 'u',
        default: process.env.WATTPM_RESOLVE_USERNAME
      },
      password: {
        type: 'string',
        short: 'p',
        default: process.env.WATTPM_RESOLVE_PASSWORD
      },
      'skip-dependencies': {
        type: 'boolean',
        short: 's',
        default: false
      },
      'package-manager': {
        type: 'string',
        short: 'P'
      }
    },
    false
  )

  const root = getRoot(positionals)
  const configurationFile = await findRuntimeConfigurationFile(logger, root, config)

  /* c8 ignore next 3 - Hard to test */
  if (!configurationFile) {
    return
  }

  const resolved = await resolveApplications(
    logger,
    root,
    configurationFile,
    username,
    password,
    skipDependencies,
    packageManager
  )

  if (resolved) {
    logger.done('All applications have been resolved.')
  }
}

export const help = {
  import: {
    usage: 'import [root] [url]',
    description: 'Imports an external resource as an application',
    args: [
      {
        name: 'root',
        description: 'The directory containing the project (the default is the current directory)'
      },
      {
        name: 'url',
        description: 'The URL to import (can be in the form $USER/$REPOSITORY for GitHub repositories)'
      }
    ],
    options: [
      {
        usage: '-c, --config <config>',
        description: 'Name of the configuration file to use (the default to autodetect it)'
      },
      {
        usage: '-i, --id <value>',
        description: 'The id of the application (the default is the basename of the URL)'
      },
      {
        usage: '-H, --http',
        description: 'Use HTTP URL when expanding GitHub repositories'
      },
      {
        usage: '-b, --branch <branch>',
        description: 'The branch to clone (the default is main)'
      },
      {
        usage: '-s, --skip-dependencies',
        description: 'Do not install applications dependencies'
      },
      {
        usage: '-P, --package-manager <executable>',
        description: 'Use an alternative package manager (the default is to autodetect it)'
      }
    ]
  },
  resolve: {
    usage: 'resolve [root]',
    description: 'Resolves all external applications',
    args: [
      {
        name: 'root',
        description: 'The directory containing the project (the default is the current directory)'
      }
    ],
    options: [
      {
        usage: '-c, --config <config>',
        description: 'Name of the configuration file to use (the default to autodetect it)'
      },
      {
        usage: '-u, --username <value>',
        description: 'The username to use for HTTP URLs'
      },
      {
        usage: '-p, --password <value>',
        description: 'The password to use for HTTP URLs'
      },
      {
        usage: '-s, --skip-dependencies',
        description: 'Do not install applications dependencies'
      },
      {
        usage: '-P, --package-manager <executable>',
        description: 'Use an alternative package manager (the default is to autodetect it)'
      }
    ],
    footer () {
      return `
${getExecutableId()} resolve command resolves runtime applications that have the \`url\` in their configuration.
To change the directory where an application is cloned, you can set the \`path\` property in the application configuration.

After cloning the application, the resolve command will set the relative path to the application in the Platformatic configuration file.

Example of the runtime \`watt.json\` configuration file:

\`\`\`json
{
  "$schema": "https://schemas.platformatic.dev/@platformatic/wattpm/2.0.0.json",
  "entrypoint": "application-1",
  "applications": [
    {
      "id": "application-1",
      "path": "./applications/application-1",
      "config": "platformatic.json"
    },
    {
      "id": "application-2",
      "config": "platformatic.json",
      "url": "https://github.com/test-owner/test-application.git"
    },
    {
      "id": "application-3",
      "config": "platformatic.json",
      "path": "./custom-external/application-3",
      "url": "https://github.com/test-owner/test-application.git"
    }
  ],
}
\`\`\`

If not specified, the configuration will be loaded from any of the following, in the current directory.

* \`watt.json\`, or
* \`platformatic.application.json\`, or
* \`platformatic.json\`, or
* \`watt.yaml\`, or
* \`platformatic.application.yaml\`, or
* \`platformatic.yaml\`, or
* \`watt.yml\`, or
* \`platformatic.application.yml\`, or
* \`platformatic.yml\`, or
* \`watt.toml\`, or
* \`platformatic.application.toml\`, or
* \`platformatic.toml\`, or
* \`watt.tml\`, or
* \`platformatic.application.tml\`, or
* \`platformatic.tml\`

You can find more details about the configuration format here:
* [Platformatic DB Configuration](https://docs.platformatic.dev/docs/db/configuration)
* [Platformatic Application Configuration](https://docs.platformatic.dev/docs/application/configuration)
    `
    }
  }
}
